using System.Collections;
using System.Reflection;
using GraphQL.DataLoader;

namespace GraphQL.Types
{
    /// <summary>
    /// Parses a <see cref="System.Type"/> along with nullability information into its constituent parts
    /// in preparation for creating a graph type that represents such a type.
    /// </summary>
    public class TypeInformation
    {
        /// <summary>
        /// The member being inspected. This is a <see cref="MethodInfo"/>, <see cref="PropertyInfo"/> or <see cref="FieldInfo"/> instance.
        /// </summary>
        public MemberInfo MemberInfo { get; }

        /// <summary>
        /// The parameter being inspected. Only applicable when <see cref="MemberInfo"/> is a <see cref="MethodInfo"/>.
        /// </summary>
        public ParameterInfo? ParameterInfo { get; }

        /// <summary>
        /// Indicates that this is an input type (an argument or input field); false for output types.
        /// </summary>
        public bool IsInputType { get; }

        /// <summary>
        /// The underlying CLR type represented. This might be the underlying type of a <see cref="Nullable{T}"/>
        /// or the underlying type of a <see cref="IEnumerable{T}"/>.
        /// </summary>
        public Type Type { get; set; }

        /// <summary>
        /// Indicates if the underlying type is nullable.
        /// </summary>
        public bool IsNullable { get; set; }

        /// <summary>
        /// Indicates that this represents a list of elements.
        /// </summary>
        public bool IsList { get; set; }

        /// <summary>
        /// Indicates if the list is nullable.
        /// </summary>
        public bool ListIsNullable { get; set; }

        private Type? _graphType;
        /// <summary>
        /// The graph type of the underlying CLR type or <see langword="null"/> to detect the graph type automatically.
        /// </summary>
        public Type? GraphType
        {
            get => _graphType;
            set
            {
                if (value == null)
                {
                    _graphType = null;
                    return;
                }
                if (IsInputType)
                {
                    if (!value.IsInputType())
                    {
                        throw new ArgumentOutOfRangeException(nameof(value), "Value can only be an input graph type.");
                    }
                }
                else if (!value.IsOutputType())
                {
                    throw new ArgumentOutOfRangeException(nameof(value), "Value can only be an output graph type.");
                }
                if (!value.IsNamedType())
                {
                    throw new ArgumentOutOfRangeException(nameof(value), "Value must be a named graph type.");
                }
                _graphType = value;
            }
        }

        /// <summary>
        /// Initializes an instance with the specified properties.
        /// </summary>
        /// <param name="memberInfo">The member being inspected.</param>
        /// <param name="isInputType">Indicates that this is an input type (an argument or input field); false for output types.</param>
        /// <param name="type">The underlying type.</param>
        /// <param name="isNullable">Indicates that the underlying type is nullable.</param>
        /// <param name="isList">Indicates that this member represents a list of elements.</param>
        /// <param name="listIsNullable">Indicates that the list is nullable.</param>
        /// <param name="graphType">The graph type of the underlying CLR type; null to be generated by <see cref="GraphQL.TypeExtensions.GetGraphTypeFromType(System.Type, bool, TypeMappingMode)"/>.</param>
        public TypeInformation(MemberInfo memberInfo, bool isInputType, Type type, bool isNullable, bool isList, bool listIsNullable, Type? graphType)
        {
            MemberInfo = memberInfo ?? throw new ArgumentNullException(nameof(memberInfo));
            IsInputType = isInputType;
            Type = type ?? throw new ArgumentNullException(nameof(type));
            IsNullable = isNullable;
            IsList = isList;
            ListIsNullable = listIsNullable;
            GraphType = graphType;
        }

        /// <summary>
        /// Initializes an instance with the specified properties.
        /// </summary>
        /// <param name="parameterInfo">The parameter being inspected.</param>
        /// <param name="type">The underlying type.</param>
        /// <param name="isNullable">Indicates that the underlying type is nullable.</param>
        /// <param name="isList">Indicates that this member represents a list of elements.</param>
        /// <param name="listIsNullable">Indicates that the list is nullable.</param>
        /// <param name="graphType">The graph type of the underlying CLR type; null to be generated by <see cref="GraphQL.TypeExtensions.GetGraphTypeFromType(System.Type, bool, TypeMappingMode)"/>.</param>
        public TypeInformation(ParameterInfo parameterInfo, Type type, bool isNullable, bool isList, bool listIsNullable, Type? graphType)
            : this(parameterInfo.Member, true, type, isNullable, isList, listIsNullable, graphType)
        {
            ParameterInfo = parameterInfo;
        }

        /// <summary>
        /// Initializes an instance containing type information necessary to select a graph type.
        /// The instance is populated based on inspecting the type and NRT annotations on the specified property.
        /// </summary>
        public TypeInformation(PropertyInfo propertyInfo, bool isInput)
            : this(propertyInfo, isInput, propertyInfo.PropertyType, false, false, false, null)
        {
            var typeTree = Interpret(new NullabilityInfoContext().Create(propertyInfo), isInput);

            ProcessTypeTree(typeTree, isInput);
        }

        /// <summary>
        /// Initializes an instance containing type information necessary to select a graph type.
        /// The instance is populated based on inspecting the type and NRT annotations on the specified field.
        /// </summary>
        public TypeInformation(FieldInfo fieldInfo, bool isInput)
            : this(fieldInfo, isInput, fieldInfo.FieldType, false, false, false, null)
        {
            var typeTree = Interpret(new NullabilityInfoContext().Create(fieldInfo), isInput);

            ProcessTypeTree(typeTree, isInput);
        }

        /// <summary>
        /// Initializes an instance containing type information necessary to select a graph type.
        /// The instance is populated based on inspecting the type and NRT annotations on the specified method.
        /// </summary>
        public TypeInformation(MethodInfo methodInfo)
            : this(methodInfo, false, methodInfo.ReturnType, false, false, false, null)
        {
            var typeTree = Interpret(new NullabilityInfoContext().Create(methodInfo.ReturnParameter), false);

            ProcessTypeTree(typeTree, false);
        }

        /// <summary>
        /// Initializes an instance containing type information necessary to select a graph type.
        /// The instance is populated based on inspecting the type and NRT annotations on the specified parameter.
        /// </summary>
        public TypeInformation(ParameterInfo parameterInfo)
            : this(parameterInfo, parameterInfo.ParameterType, false, false, false, null)
        {
            var typeTree = Interpret(new NullabilityInfoContext().Create(parameterInfo), true);

            ProcessTypeTree(typeTree, true);
        }

        /// <summary>
        /// Populates the <see cref="Type"/>, <see cref="IsNullable"/>, <see cref="IsList"/> and <see cref="ListIsNullable"/>
        /// properties of this instance from a provided <paramref name="typeTree"/>.
        /// </summary>
        private void ProcessTypeTree(List<(Type Type, NullabilityState Nullable)> typeTree, bool isInput)
        {
            foreach (var type in typeTree)
            {
                //detect list types, but not lists of lists
                if (!IsList)
                {
                    if (type.Type.IsArray)
                    {
                        //unwrap type and mark as list
                        IsList = true;
                        ListIsNullable = IsNullable || type.Nullable != NullabilityState.NotNull;
                        continue;
                    }
                    if (type.Type.IsGenericType)
                    {
                        if (IsRecognizedListType(type.Type))
                        {
                            //unwrap type and mark as list
                            IsList = true;
                            ListIsNullable = IsNullable || type.Nullable != NullabilityState.NotNull;
                            IsNullable = false;
                            continue;
                        }
                    }
                    if (type.Type == typeof(IEnumerable) || type.Type == typeof(ICollection))
                    {
                        //assume list of nullable object
                        IsList = true;
                        ListIsNullable = IsNullable || type.Nullable != NullabilityState.NotNull;
                        IsNullable = false;
                        break;
                    }
                }
                if (!isInput)
                {
                    if (type.Type.IsGenericType)
                    {
                        var genericType = type.Type.GetGenericTypeDefinition();
                        if (genericType == typeof(Task<>) || genericType == typeof(ValueTask<>)
                            || genericType == typeof(IAsyncEnumerable<>)
                            || genericType == typeof(IDataLoaderResult<>) || genericType == typeof(IObservable<>))
                        {
                            //unwrap type
                            IsNullable |= type.Nullable != NullabilityState.NotNull;
                            continue;
                        }
                    }
                    if (type.Type == typeof(IDataLoaderResult))
                    {
                        //assume nullable object
                        break;
                    }
                }
                //found match
                IsNullable |= type.Nullable != NullabilityState.NotNull;
                Type = type.Type;
                return;
            }
            //unknown type
            IsNullable = true;
            Type = typeof(object);
        }

        /// <summary>
        /// Flattens a complex <see cref="NullabilityInfo"/> structure into a list of types and nullability flags.
        /// <see cref="Nullable{T}"/> structs return their underlying type rather than <see cref="Nullable{T}"/>.
        /// </summary>
        private static List<(Type Type, NullabilityState Nullable)> Interpret(NullabilityInfo info, bool isInput)
        {
            var list = new List<(Type, NullabilityState)>(info.GenericTypeArguments.Length + 1);
            RecursiveLoop(info);
            return list;

            void RecursiveLoop(NullabilityInfo info)
            {
                if (info.Type.IsGenericType)
                {
                    var nullableType = Nullable.GetUnderlyingType(info.Type);
                    if (nullableType != null)
                    {
                        list.Add((nullableType, NullabilityState.Nullable));
                    }
                    else
                    {
                        list.Add((info.Type, isInput ? info.WriteState : info.ReadState));
                    }
                    foreach (var t in info.GenericTypeArguments)
                    {
                        RecursiveLoop(t);
                    }
                }
                else if (info.ElementType != null)
                {
                    list.Add((info.Type, isInput ? info.WriteState : info.ReadState));
                    RecursiveLoop(info.ElementType);
                }
                else
                {
                    list.Add((info.Type, isInput ? info.WriteState : info.ReadState));
                }
            }
        }

        /// <summary>
        /// Applies <see cref="GraphQLAttribute"/> attributes for the specified member to this instance.
        /// Also scans the member's owning module and assembly for globally-applied attributes,
        /// and applies attributes defined within <see cref="GlobalSwitches.GlobalAttributes"/>.
        /// </summary>
        public virtual void ApplyAttributes()
        {
            var memberOrParameter = (ICustomAttributeProvider?)ParameterInfo ?? MemberInfo;
            if (memberOrParameter.IsDefined(typeof(System.ComponentModel.DataAnnotations.RequiredAttribute), false))
            {
                if (IsList)
                {
                    ListIsNullable = false;
                }
                else
                {
                    IsNullable = false;
                }
            }

            var attributes = ParameterInfo != null
                ? ParameterInfo.GetGraphQLAttributes()
                : MemberInfo.GetGraphQLAttributes();
            foreach (var attr in attributes)
            {
                attr.Modify(this);
            }
        }

        /// <summary>
        /// Returns a graph type constructed based on the properties set within this instance.
        /// If <see cref="GraphType"/> is <see langword="null"/>, the graph type is generated via
        /// <see cref="GraphQL.TypeExtensions.GetGraphTypeFromType(System.Type, bool, TypeMappingMode)"/>.
        /// The graph type is then wrapped with <see cref="NonNullGraphType{T}"/> and/or
        /// <see cref="ListGraphType{T}"/> as appropriate.
        /// </summary>
        public virtual Type ConstructGraphType()
        {
            var type = GraphType;
            if (type != null)
            {
                if (!IsNullable)
                    type = typeof(NonNullGraphType<>).MakeGenericType(type);
            }
            else
            {
                type = Type.GetGraphTypeFromType(IsNullable, IsInputType ? TypeMappingMode.InputType : TypeMappingMode.OutputType);
            }
            if (IsList)
            {
                type = typeof(ListGraphType<>).MakeGenericType(type);
                if (!ListIsNullable)
                    type = typeof(NonNullGraphType<>).MakeGenericType(type);
            }
            return type;
        }

        internal static readonly Type[] EnumerableListTypes = new Type[] {
            typeof(IEnumerable<>),
            typeof(IList<>),
            typeof(List<>),
            typeof(ICollection<>),
            typeof(IReadOnlyCollection<>),
            typeof(IReadOnlyList<>),
            typeof(HashSet<>),
            typeof(ISet<>),
        };

        /// <summary>
        /// Determines if the specified type is one of a certain set of recognized generic list types.
        /// Does not match for <see cref="string"/> or <see cref="IDictionary{TKey, TValue}"/> or other
        /// types which may also be able to be cast to <see cref="IEnumerable{T}"/>.
        /// </summary>
        private bool IsRecognizedListType(Type type)
            => Array.IndexOf(EnumerableListTypes, type.GetGenericTypeDefinition()) >= 0;
    }
}
